Phase 6 — Chat Refresh

Arc Post-Mortem Summary. This arc refreshed the one-to-one chat. It moved message delivery onto a plain-text push channel and preferred WebSockets over server-sent events (I), and rebuilt the input row into a single rounded pill: a plus button opens the attachment dialog, the emoji button and an auto-growing textarea sit in the pill, and the trailing control swaps between a microphone and a send arrow as text is entered (II). It removed the per-conversation translation feature and its per-message API rather than reworking them (IV), which left nothing tertiary in the header, so the planned overflow menu was dropped (III); the message bubbles were recolored through the Phase 5 custom properties, the sender's own tinted and the other party's neutral (V). Alongside the planned work it added smart-HTTP serving so the repository's wiki pages can be git-cloned, per-domain favicon serving, and it centered the header's video-call symbol after finding that a setDisplay call was overriding the button's flex layout with block.

Arc started 2026-07-21, after Phase 5. Sections are Roman numerals and steps within them are lettered; each step's title says what it does, followed by a sentence on what was proposed and, once worked, a few on what was done and anything learned. Marker legend: being worked on, implemented and waiting to be reviewed, done, closed by deciding not to do it, ? open decision, unmarked = planned.

What this arc is for

Yioop's group messaging works but shows its age. The message that carries a chat still arrives by polling in the common case: plain text messaging asks the server for new messages on a timer, and the server's push channel is only opened while a voice or video call is up. The input row is a run of separate controls — a paper-clip link, an emoji button, a single-line text field, and a microphone button sitting beside each other — rather than the single rounded field a person expects from a modern chat. The header carries every action inline, the per-conversation translation control reads as clutter, and the message bubbles do not yet use the color and spacing groundwork Phase 5 laid down. This arc brings the chat up to date on both counts: how a message arrives (push instead of poll, where the server can push), and how the chat looks and feels to use. It builds on Phase 5's custom properties for the visual work and on atto's WebSocket support for the delivery work.

I. Deliver messages by push, not by polling

  1. A. Push plain text messages instead of polling for them. The wait the server-sent event stream tells the browser to hold before reconnecting is now a named constant, USER_MESSAGE_STATUS_POLL_TIME. The full push, without any reconnect, ships in I.B. To-do item 2. Done.
  2. B. Prefer WebSockets over server-sent events where the server can hold a socket. Under the command-line server, a /ws-messages WebSocket route holds a conversation's socket open and pushes each new message the moment it lands; browsers on a server that cannot hold a socket keep using the event stream. This needed one general addition to the atto server: a WebSocket route now sees its handshake request through the usual request globals, so it can authenticate the connection. To-do item 1. Done.

II. Rebuild the chat input row

  1. A. Make the input a single rounded pill. Settled with Chris as ⊕ (🙂 New message 🎤): a filled circle sits to the left of the pill and opens the attachment dialog directly, and the pill (#user-input-container, a --gray-93 surface with a --gray-80 border) holds the emoji button, the growing text field, and the trailing record/send control. Keeping the emoji and attach as their own one-tap controls avoids a nested "emoji or attachment" chooser. In UsermessagesElement the file input and its now-plus attach left the pill's left edge to become that circle, and the record, audio, and send controls moved inside the pill; in messages.css the attach became the round circle, the emoji, mic, and send became transparent inline icons, and the text field grows to fill (flex: 1; min-width: 0). Rendered headless at 360px and 800px with no viewport overflow. To-do item 6.
  2. B. Grow the field as the message grows. The single-line <input type="text"> became a <textarea> whose height is set to its own scrollHeight on each input by autoGrowMessageField, so it starts one line tall and grows with the text up to a 6rem cap, after which it scrolls; the pill grows with it and bottom-aligns its controls. A keydown handler sends on Enter (through the form's requestSubmit, only when the field is not empty) and lets Shift+Enter start a new line. Rendered headless at one, three, and many lines with no viewport overflow and the height capping as intended. To-do item 6.
  3. C. Swap the microphone for a send button when there is text to send. Done together with II.A, since the two share the trailing slot inside the pill. The field's input event drives updateMessageSendControl, which shows the microphone (record) while the field is empty and the send button once anything is typed. So the recorded-audio case also flips to send and back, initializeAudioRecorder now fires an input event when it fills the field on stop and when it clears the field on delete, and the two direct record_button.style.display writes it used to make, now redundant, were removed. To-do item 6.

III. Tidy the chat header

  1. A. Move the header's lesser actions behind a kebab menu. Closed as not needed. With the translation control removed in IV, the header carries only primary things — the contact identity, the video-call button, and on a phone the back arrow — so nothing tertiary is left to move behind an overflow menu. If the header later gains secondary actions, an overflow menu can be reconsidered then. To-do item 7.

IV. Rethink per-message translation

  1. A. Replace the per-conversation translation dropdown. Decided with Chris to remove the feature entirely rather than build a per-message replacement. Removed: the translation-language dropdown from the chat header; the SocialComponent translation methods (setTranslationLanguage, translateMessageBatch, getTranslationLocales, translateText) and the two message-path blocks that translated a loaded batch; the GroupModel per-contact translation storage (get/setTranslationLanguage); the ApiController translate action and its translateText, with translate dropped from that controller's activity list; and the translation-language CSS. The general localization system (tl(), locale files) is untouched. To-do item 8.

V. Polish the bubbles and spacing

  1. A. Color and space the bubbles with the Phase 5 custom properties. The bubble background classes moved from the hardcoded back-light-blue/back-light-gray utilities to semantic message-mine/message-theirs classes, set in both the initial render (UsermessagesElement) and the refresh append (SocialComponent). The sender's own messages now take a tinted bubble (--color-topbar) and the other party's a neutral one (--gray-93), each with a --gray-80 border for definition, so both track light and dark. Time labels between message groups are unchanged. The quoted-reply visual has no target yet — the chat has no reply/quote feature to style — so that part waits until one exists. The light-mode tint is deliberately subtle (the pale custom properties); say if you want more contrast between the two sides. To-do item 9.

Order and dependencies

The delivery work (I) and the visual work (II–V) are independent and can proceed in either order. Within delivery, push plain messaging (I.A) comes before choosing the channel (I.B), since the channel choice is a refinement of the push path. Within the input row, the pill (II.A) comes before the growing field and the mic/send swap (II.B, II.C) sit inside it. The translation decision (IV) is an open question to settle with Chris before it is worked, so it is marked with a question rather than planned as written. The bubble polish (V) depends only on the Phase 5 custom properties, which are in place.

Other work accomplished

  1. Smart-HTTP serving for the git repository wiki pages, in pure PHP. Yioop serves a group's git repository over the dumb side of git's HTTP protocol, so a client must download the whole history and cannot make a shallow (--depth) clone. This adds the smart side, in pure PHP with no git program involved.
    1. A new pure-logic library class, GitUploadPack, with unit tests. It frames git's pkt-lines, advertises the repository's refs with the shallow capability, reads a client's want-and-depth request, walks history to the requested depth (reporting the shallow boundary), and writes a fresh packfile of just those objects, reading objects through the existing GitRepository. Done.
    2. Wiring into GitComponent so a real git clone --depth against a repository wiki page works: the ref advertisement answers GET info/refs?service=git-upload-pack, and the packfile answers POST git-upload-pack. Done.
  2. Per-domain favicon serving. When a browser asks a Yioop web server for /favicon.ico, ResourceController now looks up the request host's saved appearance and serves that domain's own favicon from its per-domain resource subfolder, falling back to the global icon when the domain has none or the file is missing. This matters for pages that carry no favicon link of their own, such as a served static HTML file, which would otherwise all show the one shared icon whatever domain served them. A follow-up guard keeps a found per-domain icon from being overwritten by the later global-favicon path. Done.
  3. Center the video-call symbol in its button. Two causes. First, on a narrow or short viewport the shared icon-button rule shrank every .icon-button-container to 1.2rem wide while its height stayed 2rem, so the call button became a portrait box; the call-control buttons now keep the square --control-height on every viewport. Second, and the real cause of the symbol sitting centered on load then jumping to the top a moment later: setCallState runs on load and calls setDisplay('video-start', true), whose default display type is block, so it replaced the button's inline-flex with block and the flex centering stopped applying. The four show-calls for video-start and video-end now pass 'inline-flex' as the display type, so the buttons stay flex and the glyph stays centered. Measured headless: under block the glyph rides 5px high, under inline-flex it is centered.